﻿Imports System.Runtime.CompilerServices

Module CodeBuilderDriver

	'TODO: FieldInfo classes should be singletons and accessible from the base class by name and number.
	'TODO: Change the xxxField modules into classes that inherit from FieldInfo.
	'TODO: All classes should have an instance of FieldInfo
	Public FieldList As List(Of Code_Generator.FieldBuilder)

	Sub Main()
		Dim dataDictionary = XDocument.Load("C:\Source\FixSharp\Jonathans Scratch Space\FIX44_New.xml")
		'Dim enumFile As String = "C:\Source\FixSharp\Jonathans Scratch Space\ScratchCompiler\Enums.vb"
		Dim fieldInfoFile As String = "C:\Source\FixSharp\Jonathans Scratch Space\ScratchCompiler\Fields.vb"
		Dim fieldInfoFileFolder As String = "C:\Source\FixSharp\Jonathans Scratch Space\ScratchCompiler\Fields"


		fieldList = (From node In dataDictionary.<Fix>.<Fields>.<Field> Select New FieldBuilder(node)).OrderBy(Function(item) item.FieldName).ToList

		''Enumerations
		'Dim enumCode As New Text.StringBuilder
		'For Each field In fieldList
		'	enumCode.Append(field.BuildEnumeration)
		'Next
		'IO.File.WriteAllText(enumFile, enumCode.ToString)

		'Field Info
		'Dim fieldInfoCode As New Text.StringBuilder

		Dim fieldInfoCode As New Text.StringBuilder
		For Each item In fieldList
			fieldInfoCode.AppendLine(String.Format("temp.Add(new {0}FieldInfo())", item.FieldName))
		Next
		IO.File.WriteAllText(fieldInfoFile, String.Format(fieldInfoBase, fieldInfoCode.ToString).NormalizeLineBreaks)

		'fieldInfoCode.AppendLine(String.Format(fieldInfoBase, temp.ToString))

		IO.Directory.CreateDirectory(fieldInfoFileFolder)
		For Each item In fieldList
			Dim temp As New Text.StringBuilder
			temp.AppendLine(item.BuildAsFieldInfo)
			temp.AppendLine(item.BuildEnumeration)
			IO.File.WriteAllText(IO.Path.Combine(fieldInfoFileFolder, item.FieldName & ".vb"), temp.ToString.NormalizeLineBreaks)
		Next

		'Components
		Dim componentFileFolder As String = "C:\Source\FixSharp\Jonathans Scratch Space\ScratchCompiler\Components"
		IO.Directory.CreateDirectory(componentFileFolder)

		Dim componentList = (From node In dataDictionary.<Fix>.<Components>.<Component> Select New ComponentBuilder(node)).OrderBy(Function(item) item.Name).ToList
		componentList.Add(New ComponentBuilder("Header", dataDictionary.<Fix>.<Header>.First))
		componentList.Add(New ComponentBuilder("Trailer", dataDictionary.<Fix>.<Trailer>.First))


		For Each item In componentList
			IO.File.WriteAllText(IO.Path.Combine(componentFileFolder, item.Name & ".vb"), item.BuildAsClass)
		Next

		'Messages
		Dim messageFileFolder As String = "C:\Source\FixSharp\Jonathans Scratch Space\ScratchCompiler\Messages"
		IO.Directory.CreateDirectory(messageFileFolder)

		Dim messageList = (From node In dataDictionary.<Fix>.<Messages>.<Message> Select New MessageBuilder(node)).OrderBy(Function(item) item.Name).ToList


		For Each item In messageList
			IO.File.WriteAllText(IO.Path.Combine(messageFileFolder, item.Name & ".vb"), item.BuildAsClass)
		Next


	End Sub


	<Extension()> Public Function NormalizeLineBreaks(ByVal value As String) As String
		Return value.Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf)
	End Function

	<Extension()> Public Function NameVBFormat(ByVal type As Type) As String
		Return type.Name.Replace("[", "(").Replace("]", ")")
	End Function

	Public Function FixToClrType(ByVal fixType As FixDataType) As Type
		Select Case fixType
			Case FixDataType.Amt : Return GetType(Decimal)
			Case FixDataType.[Boolean] : Return GetType(Boolean)
			Case FixDataType.[Char] : Return GetType(Char)
			Case FixDataType.Country : Return GetType(String)
			Case FixDataType.Currency : Return GetType(String)
			Case FixDataType.DayOfMonth : Return GetType(Integer)
			Case FixDataType.Data : Return GetType(Byte())
			Case FixDataType.Exchange : Return GetType(String)
			Case FixDataType.Float : Return GetType(Decimal)
			Case FixDataType.Int : Return GetType(Integer)
			Case FixDataType.Length : Return GetType(Integer)
			Case FixDataType.LocalMktDate : Return GetType(Date)
			Case FixDataType.MonthYear : Return GetType(Date)
			Case FixDataType.MultipleValueString : Return GetType(String())
			Case FixDataType.NumInGroup : Return GetType(Integer)
			Case FixDataType.Percentage : Return GetType(Decimal)
			Case FixDataType.Price : Return GetType(Decimal)
			Case FixDataType.PriceOffset : Return GetType(Decimal)
			Case FixDataType.Qty : Return GetType(Decimal)
			Case FixDataType.SeqNum : Return GetType(Integer)
			Case FixDataType.[String] : Return GetType(String)
			Case FixDataType.TagNumber : Return GetType(Integer)
			Case FixDataType.UtcDateOnly : Return GetType(DateTimeOffset)
			Case FixDataType.UtcTimeOnly : Return GetType(DateTimeOffset)
			Case FixDataType.UtcTimestamp : Return GetType(DateTimeOffset)
			Case Else
				Throw New ArgumentOutOfRangeException("fixType")
		End Select
	End Function


	Dim fieldInfoBase As String = <s>
	Public MustInherit Class FieldInfo
		Public MustOverride ReadOnly Property ClrType() As Type
		Public MustOverride ReadOnly Property FixType() As FixDataType
		Public MustOverride ReadOnly Property TagNumber() As Integer
		Public MustOverride ReadOnly Property FieldName() As String

	Private Shared ReadOnly m_FieldByTag As Dictionary(Of Integer, FieldInfo)
	Private Shared ReadOnly m_FieldByName As Dictionary(Of String, FieldInfo)

	Shared Sub New()
		Dim temp As New List(Of FieldInfo)

		{0}

		m_FieldByTag = temp.ToDictionary(Function(item) item.TagNumber)
		m_FieldByName = temp.ToDictionary(Function(item) item.FieldName)
	End Sub

	Public Function FindByTag(ByVal tagNumber As Integer) As FieldInfo
		Return m_FieldByTag(tagNumber)
	End Function
	Public Function FindByName(ByVal fieldName As String) As FieldInfo
		Return m_FieldByName(fieldName)
	End Function


	End Class
</s>.Value

	Public fieldInfoTemplate As String = <s>
Public Class {1}FieldInfo
	Inherits FieldInfo

	Public Overrides ReadOnly Property ClrType() As Type
		Get
		Return GetType({3})
		End Get
	End Property
	Public Overrides ReadOnly Property FixType() As FixDataType
		Get
		Return FixDataType.{2}
		End Get
	End Property
	Public Overrides ReadOnly Property TagNumber() As Integer
		Get
		Return {0}
		End Get
	End Property
	Public Overrides ReadOnly Property FieldName() As String
		Get
		Return "{1}"
		End Get
	End Property

	{4}

End Class
</s>.Value


End Module



Class ComponentBuilder
	Private m_Name As String
	Private m_Node As XElement

	Public ReadOnly Property Name() As String
		Get
			Return m_Name
		End Get
	End Property

	Public Sub New(ByVal name As String, ByVal node As XElement)
		m_Name = name
		m_Node = node
	End Sub


	Public Sub New(ByVal node As XElement)
		m_Name = node.@Name
		m_Node = node
	End Sub

	Public Function BuildAsClass() As String
		Dim result As New Text.StringBuilder
		result.AppendLine(BuildClassHeader)
		result.AppendLine(BuildFields)
		result.AppendLine(BuildClassFooter)
		Return result.ToString
	End Function

	Private Function BuildClassHeader() As String
		Dim result As New Text.StringBuilder
		result.AppendLine("Public Class " & Name)
		result.AppendLine("Inherits Component")
		Return result.ToString
	End Function

	Private Function BuildClassFooter() As String
		Return "End Class"
	End Function

	Private Function BuildFields() As String
		Dim result As New Text.StringBuilder
		Dim fieldIndex As Integer

		For Each item In m_Node.Elements
			If item.Name.LocalName = "Field" Then
				Dim fieldName As String = item.@Name
				Dim field = FieldList.Where(Function(f) f.FieldName = fieldName).First
				result.AppendLine(field.BuildAsProperty(CBool(item.@Required), fieldIndex))

			ElseIf item.Name.LocalName = "Group" Then
				result.AppendLine("'Group: " & item.@Name)
				result.AppendLine(GroupBuilder(item, fieldIndex))
				result.AppendLine()

			ElseIf item.Name.LocalName = "Component" Then
				result.AppendLine("'Component: " & item.@Name)
				result.AppendLine(ComponentBuilder.BuildAsProperty(item.@Name, CBool(item.@Required), fieldIndex))
			End If
			fieldIndex += 1
		Next
		Return result.ToString
	End Function

	Public Shared Function BuildAsProperty(ByVal componentName As String, ByVal required As Boolean, ByVal fieldIndex As Integer) As String
		Dim result As New Text.StringBuilder
		'TODO - how are we going to handle the required flag?

		result.AppendLine("Private m_" & componentName & " As " & componentName)
		result.AppendLine()
		result.AppendLine("<FieldInfo(GetType(" & componentName & "), " & fieldIndex & ")> _")
		result.AppendLine("Public Property " & componentName & "() As " & componentName)
		result.AppendLine("Get")
		result.AppendLine("Return m_" & componentName)
		result.AppendLine("End Get")
		result.AppendLine("Set (value As " & componentName & ")")
		'TODO - add validation logic here
		result.AppendLine("m_" & componentName & " = value")
		result.AppendLine("End Set")
		result.AppendLine("End Property")

		Return result.ToString

	End Function


End Class

Class MessageBuilder
	Private m_Name As String
	Private m_Node As XElement

	Public ReadOnly Property Name() As String
		Get
			Return m_Name
		End Get
	End Property

	Public Sub New(ByVal name As String, ByVal node As XElement)
		m_Name = name
		m_Node = node
	End Sub


	Public Sub New(ByVal node As XElement)
		m_Name = node.@Name
		m_Node = node
	End Sub

	Public Function BuildAsClass() As String
		Dim fieldIndex As Integer
		Dim result As New Text.StringBuilder
		result.AppendLine(BuildClassHeader(fieldIndex))
		result.AppendLine(BuildFields(fieldIndex))
		result.AppendLine(BuildClassFooter(fieldIndex))
		Return result.ToString
	End Function

	Private Function BuildClassHeader(ByRef fieldIndex As Integer) As String
		Dim result As New Text.StringBuilder
		result.AppendLine("Public Class " & Name)
		result.AppendLine("Inherits Message")
		result.AppendLine()
		result.AppendLine("'Component: " & "Header")
		result.AppendLine(ComponentBuilder.BuildAsProperty("Header", True, fieldIndex))
		fieldIndex += 1
		result.AppendLine()
		Return result.ToString
	End Function

	Private Function BuildClassFooter(ByRef fieldIndex As Integer) As String
		Dim result As New Text.StringBuilder
		result.AppendLine("'Component: " & "Trailer")
		result.AppendLine(ComponentBuilder.BuildAsProperty("Trailer", True, fieldIndex))
		fieldIndex += 1
		result.AppendLine()
		result.AppendLine("End Class")
		Return result.ToString
	End Function

	Private Function BuildFields(ByRef fieldIndex As Integer) As String
		Dim result As New Text.StringBuilder

		For Each item In m_Node.Elements
			If item.Name.LocalName = "Field" Then
				Dim fieldName As String = item.@Name
				Dim field = FieldList.Where(Function(f) f.FieldName = fieldName).First
				result.AppendLine(field.BuildAsProperty(CBool(item.@Required), fieldIndex))

			ElseIf item.Name.LocalName = "Group" Then
				result.AppendLine("'Group: " & item.@Name)
				result.AppendLine(GroupBuilder(item, fieldIndex))
				result.AppendLine()

			ElseIf item.Name.LocalName = "Component" Then
				result.AppendLine("'Component: " & item.@Name)
				result.AppendLine(ComponentBuilder.BuildAsProperty(item.@Name, CBool(item.@Required), fieldIndex))
			End If
			fieldIndex += 1
		Next
		Return result.ToString
	End Function
End Class

Class FieldBuilder
	Private m_FixType As FixDataType
	Private m_FieldName As String
	Private m_TagNumber As Integer
	Private m_EnumerationMode As EnumerationMode
	Private m_EnumValues As New List(Of Enumeration)

	Public ReadOnly Property FieldName() As String
		Get
			Return m_FieldName
		End Get
	End Property

	Public Function BuildAsProperty(ByVal required As Boolean, ByVal fieldIndex As Integer) As String
		Dim result As New Text.StringBuilder
		'TODO - how are we going to handle the required flag?

		result.AppendLine("Private m_" & m_FieldName & " As " & ClrTypeName)
		result.AppendLine()
		result.AppendLine("<FieldInfo(GetType(" & m_FieldName & "FieldInfo), " & fieldIndex & ")> _")
		result.AppendLine("Public Property " & m_FieldName & "() As " & ClrTypeName)
		result.AppendLine("Get")
		result.AppendLine("Return m_" & m_FieldName)
		result.AppendLine("End Get")
		result.AppendLine("Set (value As " & ClrTypeName & ")")
		'TODO - add validation logic here
		result.AppendLine("m_" & m_FieldName & " = value")
		result.AppendLine("End Set")
		result.AppendLine("End Property")

		Return result.ToString
	End Function

	Public ReadOnly Property ClrTypeName() As String
		Get
			Dim type As String
			If m_FixType = FixDataType.Int AndAlso m_EnumValues.Count > 0 Then
				type = FieldName
			Else
				type = FixToClrType(m_FixType).NameVBFormat
			End If
			Return type
		End Get
	End Property

	Public Function BuildAsFieldInfo() As String
		Return String.Format(fieldInfoTemplate, m_TagNumber, m_FieldName, m_FixType.ToString, ClrTypeName, BuildStringConstants)
	End Function

	Public Sub New(ByVal fieldNode As XElement)
		m_FixType = CType([Enum].Parse(GetType(FixDataType), fieldNode.@Type, False), FixDataType)
		m_FieldName = fieldNode.@Name
		m_TagNumber = CInt(fieldNode.@TagNumber)
		m_EnumerationMode = CType([Enum].Parse(GetType(EnumerationMode), fieldNode.@EnumerationMode, False), EnumerationMode)

		For Each child In fieldNode.<Enum>
			m_EnumValues.Add(New Enumeration(child))
		Next
	End Sub

	Class Enumeration
		Private ReadOnly m_FixValue As String
		Private ReadOnly m_Name As String

		Public ReadOnly Property Name() As String
			Get
				Return m_Name
			End Get
		End Property
		Public ReadOnly Property FixValue() As String
			Get
				Return m_FixValue
			End Get
		End Property

		Public Sub New(ByVal enumNode As XElement)
			m_FixValue = enumNode.@FixValue
			m_Name = enumNode.@Name


		End Sub
	End Class

	Public Function BuildStringConstants() As String
		If m_EnumerationMode = EnumerationMode.None Then Return Nothing

		Select Case m_FixType
			Case FixDataType.String, FixDataType.MultipleValueString	'Constant Collection
				Dim output As New Text.StringBuilder


				output.Append("Private ReadOnly s_ValidValues As new ObjectModel.ReadOnlyCollection(Of String)( New String(){")
				output.Append(String.Join(",", m_EnumValues.Select(Function(item) """" & item.FixValue & """").ToArray))
				output.AppendLine("})")
				output.AppendLine()

				For Each item In m_EnumValues
					output.AppendLine("Public Const [" & item.Name & "] As String = """ & item.FixValue & """")
				Next
				output.AppendLine()

				output.AppendLine("Public Function GetValidValues() As ObjectModel.ReadOnlyCollection(Of String)")
				output.AppendLine("Return s_ValidValues")
				output.AppendLine("End Function")
				output.AppendLine()

				output.AppendLine("Public Function IsDefined(value As String) As Boolean")
				output.AppendLine("Return s_ValidValues.Contains(value)")
				output.AppendLine("End Function")
				output.AppendLine()

				output.AppendLine("Public Function GetName(ByVal value As String) As String")
				output.AppendLine("Select Case value")
				For Each item In m_EnumValues
					output.AppendLine("Case """ & item.FixValue & """ : Return """ & item.Name & """")
				Next
				output.AppendLine("Case Else : Throw New ArgumentOutOfRangeException(""value"")")
				output.AppendLine("End Select")
				output.AppendLine("End Function")
				output.AppendLine()

				output.AppendLine("Public Function GetValue(ByVal name As String) As String")
				output.AppendLine("Select Case name")
				For Each item In m_EnumValues
					output.AppendLine("Case """ & item.Name & """ : Return """ & item.FixValue & """")
				Next
				output.AppendLine("Case Else : Throw New ArgumentOutOfRangeException(""name"")")
				output.AppendLine("End Select")
				output.AppendLine("End Function")
				output.AppendLine()



				If m_FixType = FixDataType.MultipleValueString Then
					output.AppendLine("Public Function AreAllDefined(value As String) As Boolean")
					output.AppendLine("Dim words = value.Split(New String() {"" ""}, StringSplitOptions.RemoveEmptyEntries)")
					output.AppendLine("Return AreAllDefined(words)")
					output.AppendLine("End Function")
					output.AppendLine()

					output.AppendLine("Public Function AreAllDefined(ParamArray values() As String) As Boolean")
					output.AppendLine("For Each word In values")
					output.AppendLine("If Not IsDefined(word) Then Return False")
					output.AppendLine("Next")
					output.AppendLine("Return True")
					output.AppendLine("End Function")
					output.AppendLine()

				End If


				output.AppendLine()

				Return output.ToString


			Case FixDataType.Char
				Dim output As New Text.StringBuilder


				output.Append("Private ReadOnly s_ValidValues As new ObjectModel.ReadOnlyCollection(Of Char)( New Char(){")
				output.Append(String.Join(",", m_EnumValues.Select(Function(item) """" & item.FixValue & """c").ToArray))
				output.AppendLine("})")
				output.AppendLine()

				For Each item In m_EnumValues
					output.AppendLine("Public Const [" & item.Name & "] As Char = """ & item.FixValue & """c")
				Next
				output.AppendLine()

				output.AppendLine("Public Function GetValidValues() As ObjectModel.ReadOnlyCollection(Of Char)")
				output.AppendLine("Return s_ValidValues")
				output.AppendLine("End Function")
				output.AppendLine()

				output.AppendLine("Public Function IsDefined(value As Char) As Boolean")
				output.AppendLine("Return s_ValidValues.Contains(value)")
				output.AppendLine("End Function")
				output.AppendLine()

				output.AppendLine("Public Function GetName(ByVal value As Char) As String")
				output.AppendLine("Select Case value")
				For Each item In m_EnumValues
					output.AppendLine("Case """ & item.FixValue & """c : Return """ & item.Name & """")
				Next
				output.AppendLine("Case Else : Throw New ArgumentOutOfRangeException(""value"")")
				output.AppendLine("End Select")
				output.AppendLine("End Function")
				output.AppendLine()

				output.AppendLine("Public Function GetValue(ByVal name As String) As Char")
				output.AppendLine("Select Case name")
				For Each item In m_EnumValues
					output.AppendLine("Case """ & item.Name & """ : Return """ & item.FixValue & """c")
				Next
				output.AppendLine("Case Else : Throw New ArgumentOutOfRangeException(""name"")")
				output.AppendLine("End Select")
				output.AppendLine("End Function")
				output.AppendLine()


				Return output.ToString


			Case Else
				Return Nothing
		End Select


	End Function

	Public Function BuildEnumeration() As String
		If m_EnumerationMode = EnumerationMode.None Then Return Nothing
		If m_FixType <> FixDataType.Int Then Return Nothing

		Dim output As New Text.StringBuilder()
		output.AppendLine("Public Enum " & FieldName)
		For Each item In m_EnumValues
			output.AppendLine("[" & item.Name & "] = " & item.FixValue)
		Next
		output.AppendLine("End Enum")
		output.AppendLine()
		Return output.ToString

	End Function

End Class


Public Enum EnumerationMode
	None = 0
	Lenient
	Strict
End Enum


Module FieldParser
	'Should I change this to work on binary instead of ASCII?

	Sub Main()

		Dim testStrings = New String() {"1321", "3541345354", "1", "2.", "20.", "3.1", "30.123", "e1321", "35413453we54", "1q", "2e10", "20.", "33f.1", "30.1f23"}

		Console.WriteLine("Checking Integers")
		For Each s In testStrings
			Console.WriteLine(s & vbTab & If(s_IntegerRegex.IsMatch(s), "Match", ""))
		Next
		Console.WriteLine()
		Console.WriteLine("Checking Floats")
		For Each s In testStrings
			Console.WriteLine(s & vbTab & If(s_FloatRegex.IsMatch(s), "Match", ""))
		Next
		Console.ReadLine()



	End Sub

	Private s_IntegerRegex As New Text.RegularExpressions.Regex("^\d+$", Text.RegularExpressions.RegexOptions.Compiled)
	Private s_FloatRegex As New Text.RegularExpressions.Regex("^\d+\.?\d*$", Text.RegularExpressions.RegexOptions.Compiled)


	Public Function ParseAmt(ByVal value As String, ByVal mode As FormatMode) As Decimal
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Return ParseFloat(value, mode)
	End Function

	Public Function ParseBoolean(ByVal value As String, ByVal mode As FormatMode) As Boolean
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Select Case value 'FIX is case sensitive
			Case "Y"
				Return True
			Case "N"
				Return False
		End Select

		If CBool(mode And FormatMode.LenientFormat) Then
			Select Case value.ToLower
				Case "t", "y", "1", "-1"
					Return True
				Case "f", "n", "0"
					Return False
			End Select


			Dim result As Boolean
			If Boolean.TryParse(value, result) Then Return result
		End If

		Throw New FormatException("Value cannot be converted into a boolean")
	End Function

	Public Function ParseChar(ByVal value As String, ByVal mode As FormatMode) As Char
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If value.Length = 1 Then
			Return value.Chars(0)
		Else
			Throw New FormatException("Length of value should be 1")
		End If
	End Function

	Public Function ParseCountry(ByVal value As String, ByVal mode As FormatMode) As String
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If value.Length <> 2 Then Throw New FormatException("Length of value should be 2")
		End If

		If Not CBool(mode And FormatMode.SkipEnumCheck) Then
			'TODO - Check the enumeration for this field
			'If [not found] Then Throw New FormatException("The value isn't a valid country code")
		End If

		Return value
	End Function

	Public Function ParseCurrency(ByVal value As String, ByVal mode As FormatMode) As String
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If value.Length <> 2 Then Throw New FormatException("Length of value should be 2")
		End If

		If Not CBool(mode And FormatMode.SkipEnumCheck) Then
			'TODO - Check the enumeration for this field
			'If [not found] Then Throw New FormatException("The value isn't a valid currency code")
		End If

		Return value
	End Function

	Public Function ParseDayOfMonth(ByVal value As String, ByVal mode As FormatMode) As Integer
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Dim result = ParseInt(value, mode)
		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If result < 1 Or result > 31 Then Throw New FormatException("Value must be between 1 and 31 inclusive")
		End If
		Return result
	End Function


	Public Function ParseExchange(ByVal value As String, ByVal mode As FormatMode) As String
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If value.Length <> 2 Then Throw New FormatException("Length of value should be 2")
		End If

		If Not CBool(mode And FormatMode.SkipEnumCheck) Then
			'TODO - Check the enumeration for this field
			'If [not found] Then Throw New FormatException("The value isn't a valid exchange code")
		End If

		Return value
	End Function

	Public Function ParseFloat(ByVal value As String, ByVal mode As FormatMode) As Decimal
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If Not CBool(mode And FormatMode.LenientFormat) Then
			If s_FloatRegex.IsMatch(value) Then Return Decimal.Parse(value)
		Else
			Dim result As Decimal
			If Decimal.TryParse(value, result) Then Return result
		End If

		Throw New FormatException("Value cannot be converted into an float")
	End Function

	Public Function ParseInt(ByVal value As String, ByVal mode As FormatMode) As Integer
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If Not CBool(mode And FormatMode.LenientFormat) Then
			If s_IntegerRegex.IsMatch(value) Then Return Integer.Parse(value)
		Else
			Dim result As Integer
			If Integer.TryParse(value, result) Then Return result
		End If

		Throw New FormatException("Value cannot be converted into an integer")
	End Function

	Public Function ParseLength(ByVal value As String, ByVal mode As FormatMode) As Integer
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Dim result = ParseInt(value, mode)
		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If result < 0 Then Throw New FormatException("Value must be positive")
		End If
		Return result
	End Function

	Public Function ParseLocalMktDate(ByVal value As String, ByVal mode As FormatMode) As DateTime
		'This isn't a DateTimeOffset because we don't know if it is going to be in local time or Eastern time.
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Const format As String = "yyyyMMdd"

		If value.Length = format.Length Then
			Dim result As DateTime

			If DateTime.TryParseExact(value, format, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result
		End If

		Throw New FormatException("Value cannot be converted into a Local Market Date")
	End Function

	Private s_YearMonthWeekRegex As New Text.RegularExpressions.Regex("^\d{6}w[1-5]$", Text.RegularExpressions.RegexOptions.Compiled)
	Public Function ParseMonthYear(ByVal value As String, ByVal mode As FormatMode) As Date
		'Do we need a new type to represent this correctly?

		If value Is Nothing Then Throw New ArgumentNullException("value")

		If value.Length = 6 Then 'year-month
			Dim result As Date
			If Date.TryParseExact(value, "yyyyMM", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.None, result) Then Return result

		ElseIf value.Length = 8 AndAlso s_YearMonthWeekRegex.IsMatch(value) Then 'year-month-week

			Dim result As Date
			If Date.TryParseExact(value.Substring(0, 6), "yyyyMM", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.None, result) Then
				Dim expectedMonth As Integer = result.Month

				Dim weekNumber = Integer.Parse(value.Substring(7, 1))
				If weekNumber = 1 Then Return result

				result.AddDays(7 - result.DayOfWeek) 'this gives us week two

				result.AddDays(7 * (2 - weekNumber)) 'this gives us the final week

				If result.Month <> expectedMonth Then Throw New FormatException("The value " & value & " doesn't represent a real week.")

				Return result
			End If


		ElseIf value.Length = 8 Then 'year-month-day
			Dim result As Date
			If Date.TryParseExact(value, "yyyyMMdd", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.None, result) Then Return result
		End If


		Throw New FormatException("Value cannot be converted into an month-year value")
	End Function

	Public Function ParseMultipleValueString(ByVal value As String, ByVal mode As FormatMode) As String()
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If value = "" Then
			If Not CBool(mode And FormatMode.SkipRangeCheck) Then
				Throw New FormatException("Value should be a non-empty string")
			Else
				Return New String() {}
			End If
		End If

		Return value.Split(" ".ToCharArray, StringSplitOptions.RemoveEmptyEntries)

	End Function

	Public Function ParseNumInGroup(ByVal value As String, ByVal mode As FormatMode) As Integer
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Dim result = ParseInt(value, mode)
		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If result < 0 Then Throw New FormatException("Value must be positive")
		End If
		Return result
	End Function

	Public Function ParsePercentage(ByVal value As String, ByVal mode As FormatMode) As Decimal
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Return ParseFloat(value, mode)
	End Function

	Public Function ParsePrice(ByVal value As String, ByVal mode As FormatMode) As Decimal
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Return ParseFloat(value, mode)
	End Function

	Public Function ParsePriceOffset(ByVal value As String, ByVal mode As FormatMode) As Decimal
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Return ParseFloat(value, mode)
	End Function

	Public Function ParseQty(ByVal value As String, ByVal mode As FormatMode) As Decimal
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Return ParseFloat(value, mode)
	End Function

	Public Function ParseSeqNum(ByVal value As String, ByVal mode As FormatMode) As Integer
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Dim result = ParseInt(value, mode)
		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If result < 0 Then Throw New FormatException("Value must be positive")
		End If
		Return result
	End Function

	Public Function ParseString(ByVal value As String, ByVal mode As FormatMode) As String
		If value Is Nothing Then Throw New ArgumentNullException("value")

		If value = "" Then
			If Not CBool(mode And FormatMode.SkipRangeCheck) Then
				Throw New FormatException("Value should be a non-empty string")
			Else
				Return ""
			End If
		End If
		Return value
	End Function

	Public Function ParseTagNumber(ByVal value As String, ByVal mode As FormatMode) As Integer
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Dim result = ParseInt(value, mode)
		If Not CBool(mode And FormatMode.SkipRangeCheck) Then
			If result < 0 Then Throw New FormatException("Value must be positive")
		End If
		If Not CBool(mode And FormatMode.LenientFormat) Then
			If value.StartsWith("0") Then Throw New FormatException("Value must not have leading zeros")
		End If
		Return result
	End Function

	Public Function ParseUtcDateOnly(ByVal value As String, ByVal mode As FormatMode) As DateTimeOffset
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Const withoutFractional As String = "yyyyMMdd"
		Const withFractional As String = "yyyyMMdd"

		If value.Length = withoutFractional.Length Then
			Dim result As DateTimeOffset

			If DateTimeOffset.TryParseExact(value, withoutFractional, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result

		ElseIf value.Length = withFractional.Length Then
			Dim result As DateTimeOffset

			If DateTimeOffset.TryParseExact(value, withFractional, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result

		End If

		Throw New FormatException("Value cannot be converted into a UTC Date")
	End Function

	Public Function ParseUtcTimeOnly(ByVal value As String, ByVal mode As FormatMode) As DateTimeOffset
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Const withoutFractional As String = "HH:mm:ss"
		Const withFractional As String = "HH:mm:ss.fff"

		If value.Length = withoutFractional.Length Then
			Dim result As DateTimeOffset

			If DateTimeOffset.TryParseExact(value, withoutFractional, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result

		ElseIf value.Length = withFractional.Length Then
			Dim result As DateTimeOffset

			If DateTimeOffset.TryParseExact(value, withFractional, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result

		End If

		Throw New FormatException("Value cannot be converted into a UTC Time")
	End Function

	Public Function ParseUtcTimestamp(ByVal value As String, ByVal mode As FormatMode) As DateTimeOffset
		If value Is Nothing Then Throw New ArgumentNullException("value")

		Const withoutFractional As String = "yyyyMMdd-HH:mm:ss"
		Const withFractional As String = "yyyyMMdd-HH:mm:ss.fff"

		If value.Length = withoutFractional.Length Then
			Dim result As DateTimeOffset

			If DateTimeOffset.TryParseExact(value, withoutFractional, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result

		ElseIf value.Length = withFractional.Length Then
			Dim result As DateTimeOffset

			If DateTimeOffset.TryParseExact(value, withFractional, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeUniversal, result) Then Return result

		End If

		Throw New FormatException("Value cannot be converted into a UTC Timestamp")
	End Function


End Module

<Flags()> Public Enum FormatMode
	Strict = 0
	LenientFormat = 1
	SkipRangeCheck = 2
	SkipEnumCheck = 4
End Enum



Public Module Groups

	Public Function GroupBuilder(ByVal groupNode As XElement, ByVal fieldindex As Integer) As String
		Dim result As New Text.StringBuilder

		Dim groupName = groupNode.@Name
		Dim groupVarName = "m_" & groupName
		Dim groupClassName = groupName & "Group"

		result.AppendLine("Private " & groupVarName & " As New List(Of " & groupClassName & ")")
		result.AppendLine()
		result.AppendLine("<FieldInfo(GetType(" & groupClassName & "), " & fieldIndex & ")> _")
		result.AppendLine("Public ReadOnly Property " & groupName & " As List(Of " & groupClassName & ")")
		result.AppendLine("Get")
		result.AppendLine("Return " & groupVarName)
		result.AppendLine("End Get")
		result.AppendLine("End Property")
		result.AppendLine()

		result.AppendLine(BuildAsClass(groupClassName, groupNode))

		Return result.ToString

	End Function


	Private Function BuildAsClass(ByVal className As String, ByVal groupNode As XElement) As String
		Dim result As New Text.StringBuilder
		result.AppendLine(BuildClassHeader(className))
		result.AppendLine(BuildFields(groupNode))
		result.AppendLine(BuildClassFooter)
		Return result.ToString
	End Function

	Private Function BuildClassHeader(ByVal name As String) As String
		Dim result As New Text.StringBuilder
		result.AppendLine("Public Class " & name)
		result.AppendLine("Inherits Group")
		Return result.ToString
	End Function

	Private Function BuildClassFooter() As String
		Return "End Class"
	End Function

	Private Function BuildFields(ByVal groupNode As XElement) As String
		Dim result As New Text.StringBuilder
		Dim fieldIndex As Integer
		For Each item In groupNode.Elements
			If item.Name.LocalName = "Field" Then
				Dim fieldName As String = item.@Name
				Dim field = FieldList.Where(Function(f) f.FieldName = fieldName).First
				result.AppendLine(field.BuildAsProperty(CBool(item.@Required), fieldIndex))

			ElseIf item.Name.LocalName = "Group" Then
				result.AppendLine("'Group: " & item.@Name)
				result.AppendLine(GroupBuilder(item, fieldIndex))
				result.AppendLine()

			ElseIf item.Name.LocalName = "Component" Then
				result.AppendLine("'Component: " & item.@Name)
				result.AppendLine(ComponentBuilder.BuildAsProperty(item.@Name, CBool(item.@Required), fieldIndex))
			End If
			fieldIndex += 1
		Next
		Return result.ToString
	End Function

End Module
